#!/usr/bin/env bash

# somewhat portable way to detect CPU count
detect_cpu_count () {
    if [ "$CPUS" = "" ]; then
        # Windows standard environment variable
        CPUS="$NUMBER_OF_PROCESSORS"
    fi
    if [ "$CPUS" = "" ]; then
        # Linux
        CPUS=`getconf _NPROCESSORS_ONLN 2>/dev/null`
    fi
    if [ "$CPUS" = "" ]; then
        # FreeBSD
        CPUS=`getconf NPROCESSORS_ONLN 2>/dev/null`
    fi
    if [ "$CPUS" = "" ]; then
        # nothing helped
        CPUS="1"
    fi
}

lc_run() {
    LANG=C LC_ALL=C "$@"
}

re2c="./re2c"
if test ! -x "${re2c}"; then
    echo "Cannot find re2c executable (${re2c})."
    exit 1
fi

diff_prog="diff"
valgrind=""
skeleton=0
keep_tmp_files=0
wine=""
threads=`detect_cpu_count; echo $CPUS`
tests=()
for arg in $*
do
    case $arg in
        "--valgrind" ) valgrind=`which valgrind` ;;
        "--skeleton" ) skeleton=1 ;;
        "--keep-tmp-files" ) keep_tmp_files=1 ;;
        "--wine" )
            wine=`which wine`
            re2c="${re2c}.exe"
            diff_prog="${diff_prog} -b" # ignore whitespace at the end of line
            ;;
        "-j"* )
            number=${arg#-j}
            number_pattern='^[0-9]+$'
            if [[ $number =~ $number_pattern ]]
            then
                threads=$number
            fi
            ;;
        * ) tests[${#tests[@]}]="$arg" ;; # array is continuous (old bash lacks +=)
    esac
done
echo "Running in ${threads} thread(s)"

test_blddir="test_"`date +%y%m%d%H%M%S`
rm -rf $test_blddir && mkdir $test_blddir

# preserve directory structure unless given explicit args
if [ ${#tests[@]} -eq 0 ]; then
    cp -R "@top_srcdir@/test/"* "@top_srcdir@/examples/"*  $test_blddir
else
    for f in ${tests[@]}; do
        cp $f ${f%.re}.c $test_blddir
    done
fi

chmod -R u+w $test_blddir

find $test_blddir -type f \
    ! -name '*.re' -a \
    ! -name '*.c' -a \
    ! -name '*.inc' \
    -exec rm {} \;

# if not a debug build, remove all debug subdirs
$re2c --version | grep -q "debug" \
    || find "$test_blddir" -type d -name debug | xargs rm -rf

tests=(`find $test_blddir -name '*.re' | sort`)

# set include paths, relative to build directory
cd $test_blddir \
    && incpaths=$(find * -type d -exec echo "-I ../{}" \;) \
    && cd ..

tests_per_thread=$((${#tests[@]} / threads + 1))
packs=()
for ((i = 0; i < threads; i++))
do
    j=$((i * tests_per_thread))
    packs[$i]=${tests[@]:j:tests_per_thread}
done

if test -n "${valgrind}"
then
    valgrind_options=(
        "-q"
        "--track-origins=yes"
        "--num-callers=50"
        "--leak-check=full"
        "--show-reachable=yes"
        "--malloc-fill=0xa1"
        "--free-fill=0xa1"
        )
    valgrind="${valgrind} ${valgrind_options[@]}"
    echo $valgrind
fi

run_pack() {
    local log="$1"
    shift 1

    local errcnt=0
    for x in $*
    do
        cd $test_blddir

        # remove prefix
        local outx=${x:$((${#test_blddir} + 1))}
        local outc="${outx%.re}.c"

        # filename (dot short* (long arg?)*)? ext
        # must keep to POSIX standard: no syntactic sugar like +,?, etc.
        # if you change this regexp, try it with 'sed --posix'
        local switches=`basename "$x" | lc_run sed \
            -e 's/^[^.]*\.re$//g' \
            -e 's/^[^.]*\.\(.*\)\.re$/\1/g' \
            -e 's/^\([^-]\)/-\1/' \
            -e 's/--\([^ (-]*\)/ --\1/g' \
            -e 's/(\([^)]*\))/ \1/g'`
        # check that flags do not contain uppercase letters:
        # file extensions are case-insensitive on some platforms
        printf "%s" "$switches" | lc_run grep -q "[A-Z]" \
            && { echo "bad file extension: '$outx' (uppercase letters are not allowed)"; exit 1; }
        local switches="$switches -o $outc --no-version --no-generation-date"
        # enable warnings globally
        local switches="$switches -W"

        if [ $skeleton -eq 1 ]
        then
            rm -f "$outc"

            local switches="$switches --skeleton -Werror-undefined-control-flow"

            ${valgrind} ${wine} ../${re2c} $incpaths $switches "../$x" 2>"$outc.stderr"
            local status=$(echo $?)
            [ $status -eq 0 ] && { @CC@ -Wall -Wextra -o "$outc.out" "$outc" 2>>"$outc.stderr" || status=2; }
            [ $status -eq 0 ] && { ./"$outc.out" 2>>"$outc.stderr" || status=3; }

            case $status in
                0 ) local msg="OK" ;;
                1 ) local msg="OK (expected re2c error)" ;;
                2 ) local msg="FAIL (compilation error)" ;;
                3 ) local msg="FAIL (runtime error)" ;;
                * ) local msg="FAIL (unknown error)" ;;
            esac
            printf "%-25s $outx\n" "$msg"

            if [ $status -le 1 ]; then
                [ $keep_tmp_files -eq 0 ] && rm -f "$outx" "$outc"{,.line*.{input,keys},.stderr,.out}
            else
                local errcnt=$(($errcnt + 1))
            fi
        else
            local c="${outx%.re}.c.orig"
            mv "$outc" "$c"

            # create sandbox
            tmpdir="tmp_$log" \
                && mkdir "$tmpdir" \
                && cd "$tmpdir" \
                && mkdir -p `dirname "$outx"` \
                && cp "../../$x" "$outx"
            # run re2c
            $valgrind $wine ../../$re2c $incpaths $switches "$outx" 2>"$outc.stderr" 1>&2
            # on windows output contains CR LF, cut CR to match test results
            for f in "$outc" "$outc.stderr"; do
                [ -f "$f" ] \
                    && cat "$f" | lc_run tr -d '\r' > "$f".mod \
                    && mv "$f".mod "$f"
            done
            # paste all files dropped by re2c into output file
            rm "$outx" && find . -type f \
                | lc_run sort \
                | xargs cat \
                >> "../$outc"
            # cleanup sandbox
            cd .. \
                && rm -rf "$tmpdir"

            # compare results
            local status=""
            [ -z $status ] && status=`[ -f "$c" ] || echo "MISSING"`
            [ -z $status ] && status=`$diff_prog "$c" "$outc" > "$outc.diff" || echo "FAIL"`
            [ -z $status ] && status="OK"
            if [ $status = "OK" ]
            then
                rm "$outc" "$outc.diff" "$c" "$outx"
            else
                local errcnt=$(($errcnt + 1))
            fi

            printf "%-10s $outx\n" "$status"
        fi
        cd ..
    done
    echo $errcnt > $log
}

cleanup() {
    rm -f ${logs[@]}
    kill ${wait_pids[@]}
    wait ${wait_pids[@]}
    printf "\nEh...\n"
    exit 1
}

logs=()
wait_pids=()
trap cleanup INT
for ((i = 0; i < ${#packs[@]}; i++))
do
    logs[$i]=`date +%y%m%d%H%M%S`_$i
    run_pack ${logs[i]} ${packs[i]} &
    wait_pids[${#wait_pids[@]}]=$! # array is continuous (old bash lacks +=)
done
wait ${wait_pids[@]}

errors=0
for ((i = 0; i < ${#logs[@]}; i++))
do
    error=`cat ${logs[i]}`
    errors=$((errors + error))
    rm -f ${logs[i]}
done

# remove directories that are empty or contain only .inc files
for d in $(find $test_blddir -depth -type d)
do
    [ -z "$(find $d -type f ! -name '*.inc')" ] && rm -rf "$d"
done

if [ $errors -eq 0 ]
then
    echo "All ${#tests[@]} tests passed successfully."
    exit 0
else
    echo "Error: $errors out ${#tests[@]} tests failed."
    exit 1
fi
